Skip to content

Labels feature highlight on hover - #103

Merged
xinaesthete merged 4 commits into
mainfrom
claude/labels-feature-highlight-b90fda
Jul 31, 2026
Merged

Labels feature highlight on hover#103
xinaesthete merged 4 commits into
mainfrom
claude/labels-feature-highlight-b90fda

Conversation

@xinaesthete

Copy link
Copy Markdown
Contributor

Hovering a labels feature now highlights it, the way autoHighlight already highlights a shape. Follow-on to #95, which this depends on.

Deck cannot do this job for labels. A labels tile's picking colour covers the whole quad, so there is no per-label deck object for picking_filterHighlightColor to act on, and enabling autoHighlight would light up the entire tile. The highlight is resolved per fragment instead: the bitmask shader compares the sampled instance id against a highlightedLabelId uniform. A uniform rather than a LUT entry, because the pointer moves constantly and baking hover into the table would re-upload a texture that is megabytes for a large segmentation on every move.

On the vis side the hovered label is runtime render state on a ref plus a version counter (setHoveredLabel), never Render Stack config — it would be meaningless in a saved view, and gating the re-render on crossing into a different label keeps motion within one label free. Points already carry their highlight this way.

Both canvas surfaces, one resolver (5bcda68)

SpatialCanvasViewer (headless/embedded) and SpatialCanvas (full UI) each own a handleHover. The first version wired only the viewer, so the highlight worked in the headless demo and did nothing in the full UI — which is where real datasets are actually opened. Both now call one resolveHoveredLabel in featureTooltipHover.ts; duplication is what caused this, so a single implementation is the fix rather than a tidier copy of it.

The shared resolver reads the label id off the pick object instead of going through getFeaturePickEvent, which also builds a tooltip. The viewer already paid for that call for onFeatureHover; the full UI did not, and adding a second tooltip build per pointer move for a hover tint is not worth it.

The tint prop is deck's own highlightColor (408963c)

Not a bespoke name. Deck blends its highlightColor over a highlighted fragment weighted by that colour's alpha, which is exactly what the labels shader does, and it is RGBA 0-255 on both sides — so the name carries a shared meaning, and one prop covers shapes and labels.

The subtlety worth knowing when reviewing: the labels default must be declared in the layer's defaultProps. props.highlightColor ?? MY_DEFAULT cannot work, because deck's base Layer.defaultProps fills its navy [0, 0, 128, 128] in and the prop is therefore never absent. That is what made an earlier revision draw every hover in deck's navy against a navy fill — looking exactly like the feature was dead. labelsLayer.spec pins the value arriving at the bitmask sublayer, which is the assertion that fails if the default is ever moved back to a use-site fallback.

Verification

Browser-verified, not just compiled:

  • test-fixtures/v0.7.2/blobs.zarr on both labels paths, single-scale and multiscale/tiled: the hovered label tints and outlines, the highlight follows the pointer between labels, and clears on moving off.
  • A real dataset (1113PMDC1_human_01.zarr, ~16k cell ids) in the full UI, including with the H&E image beneath — which routes through Viv, so picks carry deck's -#view-id# suffix that normalizeDeckLayerId strips.
  • With cell_labels and nuclei_labels both enabled — two elements that genuinely share label id 13032 — only the layer under the cursor lights up.

Workspace build, all tests (788), lint:biome, lint:react (only the pre-existing PointsFeatureState warning) and the docs build are green.

Reviewer notes

  • Docs updated in the three files Labels filtering and colouring, mirroring shapes #95 touched: layer-prop-flow.mdx (render-path section plus two anti-pattern rows), headless-viewer.mdx, mdv-release-checklist.mdx.
  • The useLayerData public-surface tripwire goes 17 -> 18 members for setHoveredLabel; the docblock prose carries the count in words and is updated to match.
  • Nothing new reaches a saved Render Stack, so there is no MDV persistence work implied.

🤖 Generated with Claude Code

xinaesthete and others added 4 commits July 31, 2026 15:00
Shapes have had a hover highlight since deck's `autoHighlight` was turned on for
them; labels have not, and the gesture reading differently on the two kinds is a
gap users notice immediately.

Deck cannot do this job for labels. A labels tile's picking colour covers the
whole quad, so there is no per-label deck object for `picking_filterHighlightColor`
to act on — enabling `autoHighlight` would light up the entire tile. The highlight
is therefore resolved per FRAGMENT: the bitmask shader compares the sampled
instance id against a `highlightedLabelId` uniform.

A uniform rather than a bit in the colour LUT, specifically. The pointer moves
constantly, and baking hover into the table would re-upload a texture that is
megabytes for a large segmentation on every move — the anti-pattern the whole
labels design exists to prevent. Tiles are never touched either way.

On the vis side the hovered label is runtime render state on a ref plus a version
counter (`setHoveredLabel`), never Render Stack config: it would be meaningless in
a saved view, and gating the re-render on crossing INTO A DIFFERENT LABEL keeps
motion within one label free. Points already carry their highlight this way.

The tint prop is `labelHighlightColor`, not `highlightColor`. That name belongs to
deck's own `Layer`, which defaults it to navy `[0, 0, 128, 128]` and feeds it to
`autoHighlight`; a prop of that name is never absent, so the labels default could
never win and every hover drew in deck's navy against a navy fill — the reason
this looked like it did nothing at all. `labelsLayer.spec` now pins the value that
reaches the tile sublayer, not just the plumbing.

Verified in the browser against `test-fixtures/v0.7.2/blobs.zarr`, on both the
single-scale and the multiscale (tiled) labels paths: the hovered label tints and
outlines in yellow, the highlight follows the pointer between labels, and it
clears on moving off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There are two hover implementations — `SpatialCanvasViewer` (headless/embedded)
and `SpatialCanvas` (the full UI) — each with its own `handleHover`. The previous
commit only touched the first, so the highlight worked in the headless demo I
verified against and did nothing at all in the full UI, which is where real
datasets are actually opened. `setHoveredLabel` was already on the renderer props
there; nothing ever called it.

Rather than copy the resolution into a second `handleHover`, both now call one
`resolveHoveredLabel` in `featureTooltipHover.ts`. Duplication is what caused this,
so a single implementation is the fix, not a tidier version of the same shape.

The shared resolver reads the label id straight off the pick object instead of
going through `getFeaturePickEvent`, which also builds a tooltip. The viewer
already paid for that call for `onFeatureHover`; the full UI did not, and adding a
second tooltip build per pointer move to a path that already resolves tooltips
separately is not a cost worth paying for a hover tint. A layer-config check
guards that the pick belongs to a labels layer.

Verified against a real dataset (`1113PMDC1_human_01.zarr`, ~16k cell ids) in the
full UI: hovering a cell tints it, and with `cell_labels` and `nuclei_labels` both
enabled — two elements that genuinely share label id 13032 — only the layer under
the cursor lights up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The changeset should carry the headline feature, not the implementation
reasoning; the rationale belongs in the code and the layer-prop-flow guide,
which already carry it. Same there: one sentence on why the hovered label is a
uniform rather than a LUT entry, not three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bespoke `labelHighlightColor` was the wrong conclusion from a correct
diagnosis. Deck's `highlightColor` is not merely a name collision — it means the
same thing: deck blends it over a highlighted fragment weighted by its own alpha
(`picking_filterHighlightColor`), which is what the labels shader does, and the
value is RGBA 0-255 on both sides. Shapes already set it for `autoHighlight`, so
one prop now covers both kinds instead of one name per layer type.

What actually broke was the defaulting, not the name. `highlightColor ?? DEFAULT`
can never fall back, because deck's base `Layer.defaultProps` fills its navy in and
the prop is therefore never absent. Declaring the labels default in the layer's own
`defaultProps` is what overrides it — deck merges defaultProps down the prototype
chain, child winning. As a bonus, `highlightColor` is on deck's composite ->
sublayer forwarding list, so it would reach the tile sublayers even without the
explicit threading kept here.

The regression test is retargeted accordingly: it pins that the tint arriving at
the bitmask sublayer is the labels yellow and NOT deck's navy, which is exactly the
assertion that fails if the default is ever moved back to a use-site fallback.

Verified in the full UI on 1113PMDC1_human_01.zarr with the H&E image beneath, so
the pick also carries deck's `-#view-id#` suffix: the hovered cell tints yellow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@xinaesthete, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e9e1bb79-550c-46c8-90fe-9550ea6f0afe

📥 Commits

Reviewing files that changed from the base of the PR and between baa54e9 and 408963c.

📒 Files selected for processing (18)
  • .changeset/labels-hover-highlight.md
  • docs/docs/vis/headless-viewer.mdx
  • docs/docs/vis/layer-prop-flow.mdx
  • docs/docs/vis/mdv-release-checklist.mdx
  • packages/layers/src/LabelsBitmaskTileLayer.ts
  • packages/layers/src/LabelsLayer.ts
  • packages/layers/src/index.ts
  • packages/layers/src/labelColorEncoding.ts
  • packages/layers/src/labelsBitmaskLayerShaders.ts
  • packages/layers/tests/labelColorEncoding.spec.ts
  • packages/layers/tests/labelsLayer.spec.ts
  • packages/vis/src/SpatialCanvas/SpatialCanvasViewer.tsx
  • packages/vis/src/SpatialCanvas/featureTooltipHover.ts
  • packages/vis/src/SpatialCanvas/index.tsx
  • packages/vis/src/SpatialCanvas/renderers/labelsRenderer.ts
  • packages/vis/src/SpatialCanvas/useLayerData.ts
  • packages/vis/tests/featureTooltipHover.spec.ts
  • packages/vis/tests/useLayerData.spec.tsx

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@xinaesthete
xinaesthete merged commit 5c0d6dd into main Jul 31, 2026
6 checks passed
@xinaesthete
xinaesthete deleted the claude/labels-feature-highlight-b90fda branch July 31, 2026 14:46
@github-actions github-actions Bot mentioned this pull request Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant